<template>
  <div class="vue-pull-to-wrapper" :style="{ height: wrapperHeight, transform: `translate3d(0, ${diff}px, 0)` }">
    <div v-if="topLoadMethod" :style="{ height: `${topBlockHeight}px`, marginTop: `${-topBlockHeight}px` }" class="action-block">
      <slot name="top-block" :state="state" :state-text="topText" :trigger-distance="_topConfig.triggerDistance" :diff="diff">
        <p class="default-text">{{ topText }}</p>
      </slot>
    </div>
    <div class="scroll-container">
      <slot></slot>
    </div>
    <div v-if="bottomLoadMethod" :style="{ height: `${bottomBlockHeight}px`, marginBottom: `${-bottomBlockHeight}px` }" class="action-block">
      <slot name="bottom-block" :state="state" :state-text="bottomText" :trigger-distance="_bottomConfig.triggerDistance" :diff="diff">
        <p class="default-text">{{ bottomText }}</p>
      </slot>
    </div>
  </div>
</template>

<script>
const TOP_DEFAULT_CONFIG = {
  pullText: '下拉刷新',
  triggerText: '释放更新',
  loadingText: '加载中...',
  doneText: '加载完成',
  failText: '加载失败',
  loadedStayTime: 400,
  stayDistance: 50,
  triggerDistance: 70
};

const BOTTOM_DEFAULT_CONFIG = {
  pullText: '上拉加载',
  triggerText: '释放更新',
  loadingText: '加载中...',
  doneText: '加载完成',
  failText: '加载失败',
  loadedStayTime: 400,
  stayDistance: 50,
  triggerDistance: 70
};
export default {
  props: {
    distanceIndex: {
      //滑动的阀值（值越大滑动的速度越慢）
      type: Number,
      default: 2
    },
    topBlockHeight: {
      //顶部在滚动容器外的块级元素区域高度
      type: Number,
      default: 50
    },
    bottomBlockHeight: {
      //底部在滚动容器外的块级元素区域高度
      type: Number,
      default: 50
    },
    wrapperHeight: {
      //滚动容器的高度
      type: String,
      default: '100%'
    },
    topLoadMethod: {
      //顶部下拉时执行的方法
      type: Function
    },
    bottomLoadMethod: {
      //底部上拉时执行的方法
      type: Function
    },
    isThrottleTopPull: {
      //是否截流top-pull事件的触发以保证性能，如果需要实时触发设为false
      type: Boolean,
      default: true
    },
    isThrottleBottomPull: {
      //是否截流bottom-pull事件的触发以保证性能，如果需要实时触发设为false
      type: Boolean,
      default: true
    },
    isThrottleScroll: {
      //是否截流scroll事件的触发以保证性能，如果需要实时触发设为false
      type: Boolean,
      default: true
    },
    isTopBounce: {
      //是否启用下拉回弹效果
      type: Boolean,
      default: true
    },
    isBottomBounce: {
      //是否启用上拉回弹效果
      type: Boolean,
      default: true
    },
    topConfig: {
      //滚动容器顶部信息的一些配置
      type: Object,
      default: () => {
        return {};
      }
    },
    bottomConfig: {
      //滚动容器底部信息的一些配置
      type: Object,
      default: () => {
        return {};
      }
    }
  },
  data() {
    return {
      scrollEl: null, //挂载节点
      startScrollTop: 0, //到顶部的距离
      startY: 0, //touchstart Y的位置
      currentY: 0, //touchmove Y的位置
      distance: 0, //滑动的距离
      direction: 0, //方向 distance大于0为down、小于0为up
      diff: 0, //滑动差值(触发加载时才有值)
      beforeDiff: 0,
      topText: '', //顶部加载文字
      bottomText: '', //底部加载文字
      state: '', //pull、down、loading 状态
      bottomReached: false, //到达底部标识
      throttleEmitTopPull: null, //下拉节流
      throttleEmitBottomPull: null, //上拉节流
      throttleEmitScroll: null, //滚动节流
      throttleOnInfiniteScroll: null, //滚动底部加载更多节流
      pageY: 0 //用于记录当前页面滚动位置 keep-alive
    };
  },
  computed: {
    _topConfig: function() {
      return Object.assign({}, TOP_DEFAULT_CONFIG, this.topConfig);
    },
    _bottomConfig: function() {
      return Object.assign({}, BOTTOM_DEFAULT_CONFIG, this.bottomConfig);
    }
  },
  watch: {
    state(val) {
      if (this.direction === 'down') {
        this.$emit('top-state-change', val);
      } else {
        this.$emit('bottom-state-change', val);
      }
    }
  },
  mounted() {
    this.init();
  },
  activated() {
    this.scrollEl.scrollTop = this.pageY;
  },
  methods: {
    init() {
      this.createThrottleMethods();
      this.scrollEl = this.$el.querySelector('.scroll-container');
      this.bindEvents();
    },
    createThrottleMethods() {
      this.throttleEmitTopPull = this.throttleEmit(200, 300, 'top-pull');
      this.throttleEmitBottomPull = this.throttleEmit(200, 300, 'bottom-pull');
      this.throttleEmitScroll = this.throttleEmit(100, 150, 'scroll');
      this.throttleOnInfiniteScroll = this.throttle(this.onInfiniteScroll, 400);
    },
    throttleEmit(delay, mustRunDelay = 0, eventName) {
      const throttleMethod = function() {
        const args = [...arguments];
        args.unshift(eventName);
        this.$emit.apply(this, args);
      };
      return this.throttle(throttleMethod, delay, mustRunDelay);
    },
    bindEvents() {
      this.scrollEl.addEventListener('touchstart', this.handleTouchStart);
      this.scrollEl.addEventListener('touchmove', this.handleTouchMove);
      this.scrollEl.addEventListener('touchend', this.handleTouchEnd);
      this.scrollEl.addEventListener('scroll', this.handleScroll);
    },
    //节流
    throttle(fn, delay, mustRunDelay = 0) {
      let timer = null;
      let tStart;
      return function() {
        const context = this;
        const args = arguments;
        const tCurr = +new Date();
        clearTimeout(timer);
        if (!tStart) {
          tStart = tCurr;
        }
        if (mustRunDelay !== 0 && tCurr - tStart >= mustRunDelay) {
          fn.apply(context, args);
          tStart = tCurr;
        } else {
          timer = setTimeout(function() {
            fn.apply(context, args);
          }, delay);
        }
      };
    },
    onInfiniteScroll() {
      if (this.checkBottomReached()) {
        this.$emit('infinite-scroll');
      }
    },
    checkBottomReached() {
      this.pageY = this.scrollEl.scrollTop;
      return this.scrollEl.scrollTop + this.scrollEl.offsetHeight + 1 >= this.scrollEl.scrollHeight;
    },
    handleTouchStart(event) {
      this.startY = event.touches[0].clientY;
      this.beforeDiff = this.diff;
      this.startScrollTop = this.scrollEl.scrollTop;
      this.bottomReached = this.checkBottomReached();
    },
    handleTouchMove(event) {
      this.currentY = event.touches[0].clientY;
      this.distance = (this.currentY - this.startY) / this.distanceIndex + this.beforeDiff;
      this.direction = this.distance > 0 ? 'down' : 'up';
      if (this.startScrollTop === 0 && this.direction === 'down' && this.isTopBounce) {
        event.preventDefault();
        event.stopPropagation();
        this.diff = this.distance;
        this.isThrottleTopPull ? this.throttleEmitTopPull(this.diff) : this.$emit('top-pull', this.diff);
        if (typeof this.topLoadMethod !== 'function') return;
        if (this.distance < this._topConfig.triggerDistance && this.state !== 'pull' && this.state !== 'loading') {
          this.actionPull();
        } else if (this.distance >= this._topConfig.triggerDistance && this.state !== 'trigger' && this.state !== 'loading') {
          this.actionTrigger();
        }
      } else if (this.bottomReached && this.direction === 'up' && this.isBottomBounce) {
        event.preventDefault();
        event.stopPropagation();
        this.diff = this.distance;
        this.isThrottleBottomPull ? this.throttleEmitBottomPull(this.diff) : this.$emit('bottom-pull', this.diff);
        if (typeof this.bottomLoadMethod !== 'function') return;
        if (Math.abs(this.distance) < this._bottomConfig.triggerDistance && this.state !== 'pull' && this.state !== 'loading') {
          this.actionPull();
        } else if (Math.abs(this.distance) >= this._bottomConfig.triggerDistance && this.state !== 'trigger' && this.state !== 'loading') {
          this.actionTrigger();
        }
      }
    },
    handleTouchEnd() {
      if (this.diff !== 0) {
        if (this.state === 'trigger') {
          this.actionLoading();
          return;
        }
        // pull cancel
        this.scrollTo(0);
      }
    },
    handleScroll(event) {
      this.isThrottleScroll ? this.throttleEmitScroll(event) : this.$emit('scroll', event);
      this.throttleOnInfiniteScroll();
    },
    scrollTo(y, duration = 200) {
      this.$el.style.transition = `${duration}ms`;
      this.diff = y;
      setTimeout(() => {
        this.$el.style.transition = '';
      }, duration);
    },
    actionPull() {
      this.state = 'pull';
      this.direction === 'down' ? (this.topText = this._topConfig.pullText) : (this.bottomText = this._bottomConfig.pullText);
    },
    actionTrigger() {
      this.state = 'trigger';
      this.direction === 'down' ? (this.topText = this._topConfig.triggerText) : (this.bottomText = this._bottomConfig.triggerText);
    },
    actionLoading() {
      this.state = 'loading';
      if (this.direction === 'down') {
        this.topText = this._topConfig.loadingText;
        /* eslint-disable no-useless-call */
        this.topLoadMethod.call(this, this.actionLoaded);
        this.scrollTo(this._topConfig.stayDistance);
      } else {
        this.bottomText = this._bottomConfig.loadingText;
        this.bottomLoadMethod.call(this, this.actionLoaded);
        this.scrollTo(-this._bottomConfig.stayDistance);
      }
    },
    actionLoaded(loadState = 'done') {
      this.state = `loaded-${loadState}`;
      let loadedStayTime;
      if (this.direction === 'down') {
        this.topText = loadState === 'done' ? this._topConfig.doneText : this._topConfig.failText;
        loadedStayTime = this._topConfig.loadedStayTime;
      } else {
        this.bottomText = loadState === 'done' ? this._bottomConfig.doneText : this._bottomConfig.failText;
        loadedStayTime = this._bottomConfig.loadedStayTime;
      }
      setTimeout(() => {
        this.scrollTo(0);
        // reset state
        setTimeout(() => {
          this.state = '';
        }, 200);
      }, loadedStayTime);
    }
  }
};
</script>

<style scoped>
.vue-pull-to-wrapper {
  display: flex;
  flex-direction: column;
  height: 100%;
}

.scroll-container {
  flex: 1;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}

.vue-pull-to-wrapper .action-block {
  position: relative;
  width: 100%;
}

.default-text {
  height: 100%;
  line-height: 50px;
  text-align: center;
}
.scroll-container::-webkit-scrollbar {
  width: 0;
}
</style>
